package ua.kpi.ipsa.expert.api.db;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.sleepycat.bind.tuple.IntegerBinding;
import com.sleepycat.bind.tuple.StringBinding;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryConfig;
import com.sleepycat.je.SecondaryCursor;
import com.sleepycat.je.SecondaryDatabase;
import com.sleepycat.je.SecondaryKeyCreator;
import com.sleepycat.je.Sequence;
import com.sleepycat.je.SequenceConfig;
import com.sleepycat.je.StatsConfig;

public class BDBDAO implements DAO {
	private static final String ADMIN_LOGIN = "admin";
	private static final String ADMIN_PASSWORD = "nimda";

	private final Environment myEnv;

	private final Database sequenceDB;
	private final Database algorithmDB;
	private final Database codeDB;
	private final Database codePortionDB;
	private final Database plagiarismHypoDB;
	private final Database assignedAlgorithmsDB;
	private final Database usersDB;
	private final Database codePresentationsDB;
	private final Database codePresentationEvaluationDB;

	private final SecondaryDatabase codePortionTimeIndex;
	private final SecondaryDatabase codePortionUserLoginIndex;
	private final SecondaryDatabase codePortionStudentNameIndex;
	private final SecondaryDatabase codePortionGroupIndex;
	private final SecondaryDatabase codePortionVoozIndex;
	private final SecondaryDatabase codeCodePortionIndex;
	private final SecondaryDatabase plagiarismHypoCode1Index;

	private final Sequence codeSequence;
	private final Sequence codePortionSequence;
	private final Sequence algoSequence;
	private final Sequence hypoSequence;

	private final Object codePortionLock = new Object();

	public static Environment getEnvironment(File envHome)
			throws DataAccessException {
		return getEnvironment(envHome, false);
	}

	public static Environment getEnvironment(File envHome, boolean transactional)
			throws DataAccessException {
		if (!envHome.exists() && !envHome.mkdirs())
			throw new DataAccessException(
					"Could not create database home directory.");
		try {
			EnvironmentConfig myEnvConfig = new EnvironmentConfig();
			myEnvConfig.setAllowCreate(true);
			myEnvConfig.setTransactional(transactional);
			return new Environment(envHome, myEnvConfig);
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	public BDBDAO(Environment env) throws DataAccessException {
		try {
			boolean transactional = env.getConfig().getTransactional();
			DatabaseConfig myDbConfig = new DatabaseConfig();
			SecondaryConfig mySecConfig = new SecondaryConfig();

			myDbConfig.setAllowCreate(true);
			mySecConfig.setAllowCreate(true);

			myDbConfig.setTransactional(transactional);
			mySecConfig.setTransactional(transactional);

			myEnv = env;
			sequenceDB = myEnv.openDatabase(null, "sequence", myDbConfig);
			algorithmDB = myEnv.openDatabase(null, "algorithm", myDbConfig);
			codeDB = myEnv.openDatabase(null, "code", myDbConfig);
			codePortionDB = myEnv.openDatabase(null, "codePortion", myDbConfig);
			plagiarismHypoDB = myEnv.openDatabase(null, "plagiarismHypo",
					myDbConfig);
			assignedAlgorithmsDB = myEnv.openDatabase(null,
					"assignedAlgorithms", myDbConfig);
			usersDB = myEnv.openDatabase(null, "users", myDbConfig);
			codePresentationsDB = myEnv.openDatabase(null, "codePresentations",
					myDbConfig);
			codePresentationEvaluationDB = myEnv.openDatabase(null,
					"codePresentationEvaluation", myDbConfig);

			mySecConfig.setSortedDuplicates(true);
			mySecConfig.setAllowCreate(true);
			mySecConfig.setAllowPopulate(true);

			mySecConfig.setKeyCreator(CodeTuple.CODE_PORTION_CREATOR);
			codeCodePortionIndex = myEnv.openSecondaryDatabase(null,
					"code.codePortion", codeDB, mySecConfig);

			mySecConfig.setKeyCreator(PlagiarismHypoTuple.CODE1_CREATOR);
			plagiarismHypoCode1Index = myEnv.openSecondaryDatabase(null,
					"plagiarismHypo.code1", plagiarismHypoDB, mySecConfig);

			mySecConfig.setKeyCreator(CodePortionTuple.TIME_CREATOR);
			codePortionTimeIndex = myEnv.openSecondaryDatabase(null,
					"codePortion.time", codePortionDB, mySecConfig);

			mySecConfig.setKeyCreator(CodePortionTuple.USER_LOGIN_CREATOR);
			mySecConfig
					.setBtreeComparator(CodePortionTuple.STRING_LONG_COMPARATOR);
			codePortionUserLoginIndex = myEnv.openSecondaryDatabase(null,
					"codePortion.userLogin", codePortionDB, mySecConfig);

			mySecConfig.setKeyCreator(CodePortionTuple.STUDENT_NAME_CREATOR);
			mySecConfig
					.setBtreeComparator(CodePortionTuple.STRING_LONG_COMPARATOR);
			codePortionStudentNameIndex = myEnv.openSecondaryDatabase(null,
					"codePortion.studentName", codePortionDB, mySecConfig);

			mySecConfig.setKeyCreator(CodePortionTuple.GROUP_CREATOR);
			mySecConfig
					.setBtreeComparator(CodePortionTuple.STRING_STRING_LONG_COMPARATOR);
			codePortionGroupIndex = myEnv.openSecondaryDatabase(null,
					"codePortion.group", codePortionDB, mySecConfig);

			mySecConfig.setKeyCreator(CodePortionTuple.VOOZ_CREATOR);
			mySecConfig
					.setBtreeComparator(CodePortionTuple.STRING_LONG_COMPARATOR);
			codePortionVoozIndex = myEnv.openSecondaryDatabase(null,
					"codePortion.vooz", codePortionDB, mySecConfig);

			SequenceConfig seqConfig = new SequenceConfig();
			seqConfig.setAllowCreate(true);
			seqConfig.setInitialValue(1);
			//seqConfig.setCacheSize(10);
			codeSequence = sequenceDB.openSequence(null, stringEntry("code"),
					seqConfig);
			codePortionSequence = sequenceDB.openSequence(null,
					stringEntry("codePortion"), seqConfig);
			algoSequence = sequenceDB.openSequence(null, stringEntry("algo"),
					seqConfig);
			hypoSequence = sequenceDB.openSequence(null, stringEntry("algo"),
					seqConfig);

			if (getUserPassword(ADMIN_LOGIN) == null)
				setUser(ADMIN_LOGIN, ADMIN_PASSWORD);
		} catch (DatabaseException ex) {
			close();
			throw new DataAccessException(ex);
		}
	}

	@Override
	public List<CodePortionTuple> getCodePortionsByExample(
			final CodePortionTuple example, final long toTime)
			throws DataAccessException {
		try {
			synchronized (codePortionLock) {
				final List<CodePortionTuple> list = new ArrayList<CodePortionTuple>();
				final EntityProcessor p = new EntityProcessor() {

					@Override
					public void process(DatabaseEntry key, DatabaseEntry data) {
						CodePortionTuple c = new CodePortionTuple(key, data);
						if (checkCodePortion(example, c, toTime))
							list.add(c);
					}
				};
				if (example.codePortionPK != 0) {
					DatabaseEntry key = example.getKeyEntry();
					DatabaseEntry data = new DatabaseEntry();
					if (codePortionDB.get(null, key, data, null) == OperationStatus.SUCCESS)
						p.process(key, data);
				} else {
					SecondaryDatabase sec;
					CodePortionTuple e2 = example.clone();
					e2.time = toTime;
					if (example.studentName != null)
						sec = codePortionStudentNameIndex;
					else if (example.userLogin != null)
						sec = codePortionUserLoginIndex;
					else if (example.group != null && example.vooz != null)
						sec = codePortionGroupIndex;
					else if (example.vooz != null)
						sec = codePortionVoozIndex;
					else
						sec = codePortionTimeIndex;
					indexRangeScan(sec, example.getKeyEntry(),
							example.getValueEntry(), e2.getKeyEntry(),
							e2.getValueEntry(), p);
				}
				return list;
			}
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	private static interface EntityProcessor {
		void process(DatabaseEntry key, DatabaseEntry data);
	}

	private static void indexRangeScan(SecondaryDatabase db,
			DatabaseEntry pKey1, DatabaseEntry data1, DatabaseEntry pKey2,
			DatabaseEntry data2, EntityProcessor p) {
		DatabaseEntry key1 = new DatabaseEntry();
		DatabaseEntry key2 = new DatabaseEntry();
		SecondaryKeyCreator cr = db.getConfig().getKeyCreator();
		cr.createSecondaryKey(db, pKey1, data1, key1);
		cr.createSecondaryKey(db, pKey2, data2, key2);
		indexRangeScan(db, key1, key2, p);
	}

	private static void indexRangeScan(SecondaryDatabase db, DatabaseEntry key,
			EntityProcessor p) {
		indexRangeScan(db, key, key, p);
	}

	private static void indexRangeScan(SecondaryDatabase db,
			DatabaseEntry key1, DatabaseEntry key2, EntityProcessor p) {
		SecondaryCursor cur = db.openCursor(null, null);
		try {
			DatabaseEntry key = new DatabaseEntry(Arrays.copyOfRange(
					key1.getData(), key1.getOffset(),
					key1.getOffset() + key1.getSize()));
			DatabaseEntry pKey = new DatabaseEntry();
			DatabaseEntry data = new DatabaseEntry();
			if (cur.getSearchKeyRange(key, pKey, data, null) != OperationStatus.SUCCESS)
				return;
			do {
				if (db.compareKeys(key, key2) > 0)
					return;
				p.process(pKey, data);
			} while (cur.getNext(key, pKey, data, null) == OperationStatus.SUCCESS);
		} finally {
			cur.close();
		}
	}

	private static void databaseRangeScan(Database db, DatabaseEntry key1,
			DatabaseEntry key2, EntityProcessor p) {
		Cursor cur = db.openCursor(null, null);
		try {
			DatabaseEntry key = new DatabaseEntry(Arrays.copyOfRange(
					key1.getData(), key1.getOffset(),
					key1.getOffset() + key1.getSize()));
			DatabaseEntry data = new DatabaseEntry();
			if (cur.getSearchKeyRange(key, data, null) != OperationStatus.SUCCESS)
				return;
			do {
				if (db.compareKeys(key, key2) > 0)
					return;
				p.process(key, data);
			} while (cur.getNext(key, data, null) == OperationStatus.SUCCESS);
		} finally {
			cur.close();
		}
	}

	private static boolean databaseFirstScan(Database db, DatabaseEntry key,
			DatabaseEntry data) {
		Cursor cur = db.openCursor(null, null);
		try {
			return cur.getFirst(key, data, null) == OperationStatus.SUCCESS;
		} finally {
			cur.close();
		}
	}

	private static boolean databaseNextScan(Database db, DatabaseEntry key1,
			DatabaseEntry key2, DatabaseEntry data2) {
		Cursor cur = db.openCursor(null, null);
		try {
			if (cur.getSearchKey(key1, data2, null) != OperationStatus.SUCCESS)
				return false;
			return cur.getNext(key2, data2, null) == OperationStatus.SUCCESS;
		} finally {
			cur.close();
		}
	}

	private static boolean databaseLastScan(Database db, DatabaseEntry key,
			DatabaseEntry data) {
		Cursor cur = db.openCursor(null, null);
		try {
			return cur.getLast(key, data, null) == OperationStatus.SUCCESS;
		} finally {
			cur.close();
		}
	}

	private static void databaseFullScan(Database db, EntityProcessor p) {
		Cursor cur = db.openCursor(null, null);
		try {
			DatabaseEntry key = new DatabaseEntry();
			DatabaseEntry data = new DatabaseEntry();
			while (cur.getNext(key, data, null) == OperationStatus.SUCCESS)
				p.process(key, data);
		} finally {
			cur.close();
		}
	}

	private static boolean checkCodePortion(CodePortionTuple example,
			CodePortionTuple x, long toTime) {
		return (example.codePortionPK == 0 || example.codePortionPK == x.codePortionPK)
				&& (example.userLogin == null || example.userLogin
						.equals(x.userLogin))
				&& (example.studentName == null || example.studentName
						.equals(x.studentName))
				&& (example.group == null || example.group.equals(x.group))
				&& (example.vooz == null || example.vooz.equals(x.vooz))
				&& (example.etc == null || example.etc.equals(x.vooz))
				&& example.time <= x.time && x.time <= toTime;
	}

	@Override
	public List<AssignedAlgorithmTuple> getAssignedAlgorithms(int portionFK)
			throws DataAccessException {
		try {
			final List<AssignedAlgorithmTuple> list = new ArrayList<AssignedAlgorithmTuple>();
			databaseRangeScan(assignedAlgorithmsDB, new AssignedAlgorithmTuple(
					portionFK, Integer.MIN_VALUE).getKeyEntry(),
					new AssignedAlgorithmTuple(portionFK, Integer.MAX_VALUE)
							.getKeyEntry(), new EntityProcessor() {

						@Override
						public void process(DatabaseEntry key,
								DatabaseEntry data) {
							list.add(new AssignedAlgorithmTuple(key, data));
						}
					});
			return list;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public void setAssignedAlgorithm(AssignedAlgorithmTuple algo)
			throws DataAccessException {
		try {
			assignedAlgorithmsDB.put(null, algo.getKeyEntry(),
					algo.getValueEntry());
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	private static DatabaseEntry intEntry(int x) {
		DatabaseEntry entry = new DatabaseEntry();
		IntegerBinding.intToEntry(x, entry);
		return entry;
	}

	private static DatabaseEntry stringEntry(String x) {
		DatabaseEntry entry = new DatabaseEntry();
		StringBinding.stringToEntry(x, entry);
		return entry;
	}

	@Override
	public List<PlagiarismHypoTuple> getPlagiarismHyposByCode(int code1fk)
			throws DataAccessException {
		try {
			final List<PlagiarismHypoTuple> list = new ArrayList<PlagiarismHypoTuple>();
			indexRangeScan(plagiarismHypoCode1Index, intEntry(code1fk),
					new EntityProcessor() {

						@Override
						public void process(DatabaseEntry key,
								DatabaseEntry data) {
							list.add(new PlagiarismHypoTuple(key, data));
						}
					});
			return list;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public PlagiarismHypoTuple getPlagiarismHypo(int plagiarismHypoPK)
			throws DataAccessException {
		try {
			PlagiarismHypoTuple x = new PlagiarismHypoTuple(plagiarismHypoPK);
			DatabaseEntry data = new DatabaseEntry();
			if (plagiarismHypoDB.get(null, x.getKeyEntry(), data, null) == OperationStatus.NOTFOUND)
				return null;
			x.setValueEntry(data);
			return x;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public CodeTuple getCode(int codePK) throws DataAccessException {
		try {
			CodeTuple x = new CodeTuple(codePK);
			DatabaseEntry data = new DatabaseEntry();
			if (codeDB.get(null, x.getKeyEntry(), data, null) == OperationStatus.NOTFOUND)
				return null;
			x.setValueEntry(data);
			return x;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public List<CodeTuple> getCodeByPortionKey(int codePortionFK)
			throws DataAccessException {
		try {
			final List<CodeTuple> list = new ArrayList<CodeTuple>();
			indexRangeScan(codeCodePortionIndex, intEntry(codePortionFK),
					new EntityProcessor() {

						@Override
						public void process(DatabaseEntry key,
								DatabaseEntry data) {
							list.add(new CodeTuple(key, data));
						}
					});
			return list;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public void setUser(String login, String pass) throws DataAccessException {
		try {
			usersDB.put(null, stringEntry(login), stringEntry(pass));
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public String getUserPassword(String login) throws DataAccessException {
		try {
			DatabaseEntry data = new DatabaseEntry();
			if (usersDB.get(null, stringEntry(login), data, null) == OperationStatus.NOTFOUND)
				return null;
			return StringBinding.entryToString(data);
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public List<String> getUsers() throws DataAccessException {
		try {
			final List<String> list = new ArrayList<String>();
			databaseFullScan(usersDB, new EntityProcessor() {

				@Override
				public void process(DatabaseEntry key, DatabaseEntry data) {
					list.add(StringBinding.entryToString(key));
				}
			});
			return list;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public void removeUser(String login) throws DataAccessException {
		try {
			usersDB.delete(null, stringEntry(login));
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public void setCodePortion(CodePortionTuple codePortion)
			throws DataAccessException {
		try {
			synchronized (codePortionLock) {
				codePortion.codePortionPK = (int) codePortionSequence.get(null,
						1);
				codePortionDB.put(null, codePortion.getKeyEntry(),
						codePortion.getValueEntry());
			}
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public AlgorithmTuple getAlgorithm(int algorithmPK)
			throws DataAccessException {
		try {
			AlgorithmTuple algorithm = new AlgorithmTuple(algorithmPK);
			DatabaseEntry value = new DatabaseEntry();
			if (algorithmDB.get(null, algorithm.getKeyEntry(), value, null) == OperationStatus.NOTFOUND)
				return null;
			algorithm.setValueEntry(value);
			return algorithm;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public void setCode(CodeTuple code) throws DataAccessException {
		try {
			if (code.codePK == 0)
				code.codePK = (int) codeSequence.get(null, 1);
			codeDB.put(null, code.getKeyEntry(), code.getValueEntry());
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public void setAlgorithm(AlgorithmTuple algo) throws DataAccessException {
		try {
			if (algo.algorithmPK == 0)
				algo.algorithmPK = (int) algoSequence.get(null, 1);
			algorithmDB.put(null, algo.getKeyEntry(), algo.getValueEntry());
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public List<AlgorithmTuple> getAlgorithms() throws DataAccessException {
		try {
			final List<AlgorithmTuple> list = new ArrayList<AlgorithmTuple>();
			databaseFullScan(algorithmDB, new EntityProcessor() {

				@Override
				public void process(DatabaseEntry key, DatabaseEntry data) {
					list.add(new AlgorithmTuple(key, data));
				}
			});
			return list;
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public void setPlagiarismHypo(PlagiarismHypoTuple hypo)
			throws DataAccessException {
		try {
			if (hypo.plagiarismHypoPK == 0)
				hypo.plagiarismHypoPK = (int) hypoSequence.get(null, 1);
			plagiarismHypoDB
					.put(null, hypo.getKeyEntry(), hypo.getValueEntry());
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public int getNextCodePortionID(int codePortionID)
			throws DataAccessException {
		try {
			synchronized (codePortionLock) {
				DatabaseEntry key = new DatabaseEntry();
				DatabaseEntry data = new DatabaseEntry();
				if (codePortionID == 0) {
					if (!databaseFirstScan(codePortionDB, key, data))
						return 0;
				} else if (!databaseNextScan(codePortionDB,
						intEntry(codePortionID), key, data))
					return 0;
				return IntegerBinding.entryToInt(key);
			}
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public int getLastCodePortionID() throws DataAccessException {
		try {
			synchronized (codePortionLock) {
				DatabaseEntry key = new DatabaseEntry();
				DatabaseEntry data = new DatabaseEntry();
				if (!databaseLastScan(codePortionDB, key, data))
					return 0;
				return IntegerBinding.entryToInt(key);
			}
		} catch (DatabaseException ex) {
			throw new DataAccessException(ex);
		}
	}

	@Override
	public Object getCodePortionLock() {
		return codePortionLock;
	}

	@Override
	public void commit() throws DataAccessException {
	}

	@Override
	public void rollback() throws DataAccessException {
	}

	@Override
	public void close() {
		close(codeSequence);
		close(codePortionSequence);
		close(algoSequence);
		close(hypoSequence);

		close(sequenceDB);
		close(algorithmDB);
		close(codeDB);
		close(codePortionDB);
		close(plagiarismHypoDB);
		close(assignedAlgorithmsDB);
		close(usersDB);
		close(codePresentationsDB);
		close(codePresentationEvaluationDB);
	}

	private static void close(Sequence seq) {
		if (seq != null)
			try {
				seq.close();
			} catch (DatabaseException ex) {
				ex.printStackTrace();
			}
	}

	private static void close(Database db) {
		if (db == null)
			return;
		for (SecondaryDatabase sec : db.getSecondaryDatabases())
			try {
				sec.close();
			} catch (DatabaseException ex) {
				ex.printStackTrace();
			}
		try {
			db.close();
		} catch (DatabaseException ex) {
			ex.printStackTrace();
		}
	}

	// public static void main(String[] args) throws Exception {
	// Environment env = BDBDAO.getEnvironment(new File("db"));
	// BDBDAO dao = new BDBDAO(env);
	// try {
	// CodePortionTuple cp = new CodePortionTuple(1);
	// dao.setCodePortion(cp);
	// CodeTuple c = new CodeTuple();
	// c.fileName = new String[] { "file" };
	// c.codePortionTupleFK = 1;
	// for (int i = 0; i < 1; ++i) {
	// c.codePK = 0;
	// dao.setCode(c);
	// }
	// } finally {
	// dao.close();
	// env.close();
	// }
	// }
}
